
#include "Model.h"

#include "Shader.h"

#include "Window.h"

#include "PerspectiveCamera.h"

#include "Light.h"


#ifndef UTILS_RENDERER
#define UTILS_RENDERER

#include <tuple>

#define BIT(x) (1 << (x))

enum class ClearMode {
    Color = BIT(0),
    Depth = BIT(1),
    Stencil = BIT(2)
};

inline ClearMode operator|(const ClearMode mode1, const ClearMode mode2) {
    return ClearMode((int)mode1 | (int)mode2);
}
inline ClearMode operator&(const ClearMode mode1, const ClearMode mode2) {
    return ClearMode((int)mode1 & (int)mode2);
}

enum class DrawMode {
    Triangles, Lines, Points
};

enum class EnabledTest {
    BlendTest, DepthTest, StencilTest, CullFaceTest
};

enum class TestFunction {
    ALWAYS, NEVER, LESS, EQUAL, LEQUAL, GREATER, NOTEQUAL, GEQUAL
};

enum class StencilOption {
    KEEP, ZERO, REPLACE, INCR, INCR_WRAP, DECR, DECR_WRAP, INVERT
};

enum class BlendOption {
    src_alpha, one_minus_src_alpha
};

static GLenum ToGLenum(const DrawMode drawMode) {
    switch (drawMode) {
        default: return GL_FALSE;
        case DrawMode::Triangles:	return GL_TRIANGLES;
        case DrawMode::Lines:		return GL_LINES;
        case DrawMode::Points:		return GL_POINTS;
    }
}
static GLenum ToGLenum(const ClearMode clearMode) {
    GLenum mode = 0;
    if (static_cast<int>(clearMode & ClearMode::Color))		mode |= GL_COLOR_BUFFER_BIT;
    if (static_cast<int>(clearMode & ClearMode::Depth))		mode |= GL_DEPTH_BUFFER_BIT;
    if (static_cast<int>(clearMode & ClearMode::Stencil))		mode |= GL_STENCIL_BUFFER_BIT;
    return mode;
}
static GLenum ToGLenum(const EnabledTest enabledTest) {
    GLenum test = 0;
    switch (enabledTest) {
        default: break;
        case EnabledTest::BlendTest:		test = GL_BLEND;			break;
        case EnabledTest::DepthTest:		test = GL_DEPTH_TEST;		break;
        case EnabledTest::StencilTest:		test = GL_STENCIL_TEST;		break;
        case EnabledTest::CullFaceTest:		test = GL_CULL_FACE;		break;
    }
    return test;
}
static GLenum ToGLenum(const TestFunction testFunc) {
    GLenum func = 0;
    switch (testFunc) {
        default: break;
        case TestFunction::ALWAYS:		func = GL_ALWAYS;	break;
        case TestFunction::EQUAL:		func = GL_EQUAL;	break;
        case TestFunction::GEQUAL:		func = GL_GEQUAL;	break;
        case TestFunction::GREATER:		func = GL_GREATER;	break;
        case TestFunction::LEQUAL:		func = GL_LEQUAL;	break;
        case TestFunction::LESS:		func = GL_LESS;		break;
        case TestFunction::NEVER:		func = GL_NEVER;	break;
        case TestFunction::NOTEQUAL:	func = GL_NOTEQUAL; break;
    }
    return func;
}
static GLenum ToGLenum(const StencilOption opt) {
    GLenum option = 0;
    switch (opt) {
        default: break;
        case StencilOption::DECR:		option = GL_DECR;		break;
        case StencilOption::DECR_WRAP:	option = GL_DECR_WRAP;	break;
        case StencilOption::INCR:		option = GL_INCR;		break;
        case StencilOption::INCR_WRAP:	option = GL_INCR_WRAP;	break;
        case StencilOption::INVERT:		option = GL_INVERT;		break;
        case StencilOption::KEEP:		option = GL_KEEP;		break;
        case StencilOption::REPLACE:	option = GL_REPLACE;	break;
        case StencilOption::ZERO:		option = GL_ZERO;		break;
    }
    return option;
}
static GLenum ToGLenum(const BlendOption opt) {
    GLenum option = 0;
    switch (opt) {
        default: break;
        case BlendOption::src_alpha:			option = GL_SRC_ALPHA;				break;
        case BlendOption::one_minus_src_alpha:	option = GL_ONE_MINUS_SRC_ALPHA;	break;
    }
    return option;
}

using RenderInstance = std::tuple<std::shared_ptr<VertexArray>, std::shared_ptr<Material>, Matrix4f >;
using RenderInstances = std::vector<RenderInstance>;

class Renderer {
public:
    PerspectiveCamera camera3D;
    std::shared_ptr<VertexArray> screenVAO;
    std::shared_ptr<Shader> shadowMapShader, phongShader, drawShadowMapShader;
    std::shared_ptr<ShadowMapBuffer> shadowMap;
    std::shared_ptr<Window> window;
    std::shared_ptr<Light> light;
    std::vector< std::shared_ptr<TriangleModel> > drawables;
    double ts = 0;
    
    Renderer(const std::shared_ptr<Window>& window) : window(window) {
        window->eventCallback = [this](const Event& event) { eventHander(event); };

        camera3D.configure(45.f, window->width * 1.f / window->height, 0.1f, 100.f);
        camera3D.cameraSpeed = 3.f;
        camera3D.position = Vector3f(1.f);
        camera3D.cameraFront = ZeroVector3f - camera3D.position;
        camera3D.recalculate();

        enableTest(EnabledTest::BlendTest);
        enableTest(EnabledTest::DepthTest);

        setBlendFunction(BlendOption::src_alpha, BlendOption::one_minus_src_alpha);

        shadowMapShader = std::make_shared<Shader>();
        shadowMapShader->load("../assets/shaders/genShadowMap.glsl");

        phongShader = std::make_shared<Shader>();
        phongShader->load("../assets/shaders/phongShading.glsl");

        shadowMap = std::make_shared<ShadowMapBuffer>();
        shadowMap->setAllBuffer(1024);

        {
            drawShadowMapShader = std::make_shared<Shader>();
            drawShadowMapShader->load("../assets/shaders/drawShadowMap.glsl");
            TriangleMesh screenMesh;
            auto material = std::make_shared<Material>();
            material->Ka = Vector3f(0.1f);
            material->Kd = Vector3f(208, 163, 67) / 255.f;
            material->Ks = Vector3f(0.2f);
            material->Ns = 32.f;
            material->Ni = 1.f;
            material->d = 1.f;
            material->illum = 1;
            float screenVertices[] = {
                -1.f, -1.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f,
                 1.f, -1.f, 0.f, 0.f, 0.f, 1.f, 1.f, 0.f,
                 1.f,  1.f, 0.f, 0.f, 0.f, 1.f, 1.f, 1.f,
                -1.f, -1.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f,
                 1.f,  1.f, 0.f, 0.f, 0.f, 1.f, 1.f, 1.f,
                -1.f,  1.f, 0.f, 0.f, 0.f, 1.f, 0.f, 1.f
            };
            screenMesh.load(screenVertices, 6, material);
            screenVAO = screenMesh.toVAO();
        }
    }

    void submit(const std::shared_ptr<TriangleModel>& model) {
        drawables.push_back(model);
    }

    void submit(const std::shared_ptr<Light>& _light) {
        light = _light;
    }

    void eventHander(const Event& event) {
        EventDispatcher dispatcher(event);
        dispatcher.dispatch<MouseMoveEvent>([this](const Event& evt) {
            auto e = (MouseMoveEvent&)evt;
            if (window->cursorFocusedEnable)
                camera3D.cursorTransform(e.x, e.y);
            return false;
        });
    }

    void render() {
        static auto begin = GetRunTime();

        window->pollEvents();

        control();

        std::vector<RenderInstance> renderInstances;
        for (auto& drawable : drawables)
            for (auto& triangleMesh : drawable->meshes)
                renderInstances.push_back({ triangleMesh.toVAO(), triangleMesh.material, drawable->transform });

        genShadowMap(renderInstances);
        
        phongShading(renderInstances);

        // drawShadowMap();

        window->swapBuffers();

        static auto end = GetRunTime();
        ts = end - begin;
    }

    void genShadowMap(const RenderInstances& renderInstances) {
        setViewPort({ 0.f, 0.f, shadowMap->size, shadowMap->size });  // Important.

        shadowMap->bind();
        shadowMap->bindAttachments(1);

        clearColor(Vector4f(1.f));
        clear(ClearMode::Depth);

        for (auto& renderInstance : renderInstances) {
            auto[vao, material, transform] = renderInstance;

            vao->bind();
            shadowMapShader->bind();
            shadowMapShader->setUniformMat4f("light_mvp", light->caluateLightSpaceVP(1.f) * transform);

            GLCALL(glDrawArrays(GL_TRIANGLES, 0, vao->ibo->count));

            shadowMapShader->unbind();
            vao->unbind();
        }

        shadowMap->unbind();
        shadowMap->unbindAttachments();
    }

    void phongShading(const RenderInstances& renderInstances) {
        setViewPort({ 0.f, 0.f, window->width, window->height }); 

        clearColor(Vector4f(0.2f, 0.3f, 0.4f, 1.f));
        clear(ClearMode::Color | ClearMode::Depth);

        for (auto& renderInstance : renderInstances) {
            auto[vao, material, transform] = renderInstance;

            vao->bind();
            phongShader->bind();

            phongShader->setUniformMat4f("mvp", camera3D.view_projection * transform);

            phongShader->setUniformVec3f("lightPos", light->position);
            phongShader->setUniformVec3f("cameraPos", camera3D.position);
            phongShader->setUniformVec3f("lightIntensity", light->intensity);

            phongShader->setUniformMat4f("light_mvp", light->caluateLightSpaceVP(camera3D.aspectRatio) * transform);

            phongShader->setUniformVec3f("Ka", material->Ka);
            phongShader->setUniformVec3f("Kd", material->Kd);
            phongShader->setUniformVec3f("Ks", material->Ks);
            phongShader->setUniformFloat("Ns", material->Ns);
            phongShader->setUniformFloat("Ni", material->Ni);
            phongShader->setUniformFloat("d", material->d);
            phongShader->setUniformInt("illum", material->illum);

            shadowMap->bindAttachments(1);
            phongShader->setUniformInt("shadowMap", 1);
            phongShader->setUniformInt("shadowMapSize", shadowMap->size);
            
            if (material->map_Ka) {
                material->map_Ka->bind(2);
                phongShader->setUniformInt("sample_map_Ka", 1);
                phongShader->setUniformInt("map_Ka", 2);
            }
            else {
                phongShader->setUniformInt("sample_map_Ka", 0);
            }
            
            if (material->map_Kd) {
                material->map_Kd->bind(3);
                phongShader->setUniformInt("sample_map_Kd", 1);
                phongShader->setUniformInt("map_Kd", 3);
            }
            else {
                phongShader->setUniformInt("sample_map_Kd", 0);
            }
            
            if (material->map_Ks) {
                material->map_Ks->bind(4);
                phongShader->setUniformInt("sample_map_Ks", 1);
                phongShader->setUniformInt("map_Ks", 4);
            }
            else {
                phongShader->setUniformInt("sample_map_Ks", 0);
            }
            
            if (material->map_Ns) {
                material->map_Ns->bind(5);
                phongShader->setUniformInt("sample_map_Ns", 1);
                phongShader->setUniformInt("map_Ns", 5);
            }
            else {
                phongShader->setUniformInt("sample_map_Ns", 0);
            }
            
            if (material->map_d) {
                material->map_d->bind(6);
                phongShader->setUniformInt("sample_map_d", 1);
                phongShader->setUniformInt("map_d", 6);
            }
            else {
                phongShader->setUniformInt("sample_map_d", 0);
            }
            
            if (material->map_bump) {
                material->map_bump->bind(7);
                phongShader->setUniformInt("sample_map_bump", 1);
                phongShader->setUniformInt("map_bump", 7);
            }
            else {
                phongShader->setUniformInt("sample_map_bump", 0);
            }

            GLCALL(glDrawArrays(GL_TRIANGLES, 0, vao->ibo->count));
            vao->unbind();
            phongShader->unbind();
            GLCALL(glBindTexture(GL_TEXTURE_2D, 0));
        }

    }
    
    void drawShadowMap() {
        setViewPort({ 0.f, 0.f, window->width, window->height });

        clearColor(Vector4f(1.f));
        clear(ClearMode::Color | ClearMode::Depth);

        shadowMap->bindAttachments(1);

        screenVAO->bind();
        drawShadowMapShader->bind();
        drawShadowMapShader->setUniformInt("shadowMap", 1);
        GLCALL(glDrawArrays(GL_TRIANGLES, 0, screenVAO->ibo->count));
        drawShadowMapShader->unbind();
        screenVAO->unbind();

        shadowMap->unbindAttachments();
    }

    void control() {
        if (window->isPressed(KeyboardButton::KEYB_W))
            camera3D.keyboardTransform(CameraTransform3D::Forward, ts);
        if (window->isPressed(KeyboardButton::KEYB_S))
            camera3D.keyboardTransform(CameraTransform3D::Backword, ts);
        if (window->isPressed(KeyboardButton::KEYB_A))
            camera3D.keyboardTransform(CameraTransform3D::MoveLeft, ts);
        if (window->isPressed(KeyboardButton::KEYB_D))
            camera3D.keyboardTransform(CameraTransform3D::MoveRight, ts);
        if (window->isPressed(KeyboardButton::KEYB_SPACE))
            camera3D.keyboardTransform(CameraTransform3D::MoveUp, ts);
        if (window->isPressed(KeyboardButton::KEYB_LEFT_SHIFT))
            camera3D.keyboardTransform(CameraTransform3D::MoveDown, ts);
        if (window->isPressed(KeyboardButton::KEYB_LEFT_ALT))
            window->setCursorFocusedEnable(false);
        else {
            if (!window->cursorFocusedEnable)
                camera3D.isCursorFirstInScreen = true;
            window->setCursorFocusedEnable(true);
        }

        // light->position.x = sin(glfwGetTime()) * 4.f;
        // light->position.y = 6.f;
        // light->position.z = cos(glfwGetTime()) * 8.f;
    }

    void clearColor(const Vector4f& color) {
        GLCALL(glClearColor(color[0], color[1], color[2], color[3]));
    }

    void clear(const ClearMode clearMode) {
        GLCALL(glClear(ToGLenum(clearMode)));
    }

    void setViewPort(const Vector4f& value) {
        GLCALL(glViewport(value[0], value[1], value[2], value[3]));
    }

    void enableTest(const EnabledTest enabledTest) {
        GLCALL(glEnable(ToGLenum(enabledTest)));
    }

    void disableTest(const EnabledTest disabledTest) {
        GLCALL(glDisable(ToGLenum(disabledTest)));
    }

    void setBlendFunction(const BlendOption option1, const BlendOption option2) {
        GLCALL(glBlendFunc(ToGLenum(option1), ToGLenum(option2)));
    }
};

#endif // UTILS_RENDERER
